// Default stream and parsers for Uint8TypedArray data type
import IOController from '../io/io-controller'

/**
 * options.url url
 * options.loaderType xhr|fetch
 * options.chunkSize 200 (kb)
 */
export const buildLoader = (options) => {
    let byteLength = 0
    let _buffers = null
    let error = null
    let done = false

    // 编译后的async性能较差，直接使用promise

    const ioc = new IOController({
        url: options.url,
    }, {
        loaderType: options.loaderType,
        chunkSize: options.chunkSize,
        stashInitialSize: 20,
        enableStashBuffer: false,
        seekType: 'range',
    })

    function checkBuffer(pos) {
        if (error) {
            console.error(error)
            return Promise.reject(new Error(error.message))
        }
        if (pos >= byteLength) {
            if (!done) {
                return sleep(30).then(() => checkBuffer(pos))
            }
        }
        return Promise.resolve()
    }

    function sleep(ms) {
        return new Promise((o) => setTimeout(o, ms))
    }

    return buildCustomLoader({
        load: function (buffers) {
            _buffers = buffers
            ioc._onDataArrival = (chunk, byteStart) => {
                buffers.push(new Uint8Array(chunk))
                byteLength += chunk.byteLength
            }
            ioc._onError = (t, e) => {
                error = new Error(t + ' - ' + JSON.stringify(e))
            }
            ioc._onComplete = (e) => {
                done = true
            }
            // ioc._onSeeked = null;
            // ioc._onRedirect = null;
            // ioc._onRecoveredEarlyEof = null;
            ioc.open()
        },
        check: checkBuffer,
    })
}
/**
 * options.data
 */
export const buildCustomLoader = (options) => {
    const buffers = []
    let readingBuffer = new Uint8Array([])
    let consumedBytes = 0
    let pos = 0

    // 编译后的async性能较差，直接使用promise

    options.load(buffers)

    function checkBuffer(len) {
        return options.check(consumedBytes + pos + len)
    }

    const that = {
        readByte: () => {
            return checkBuffer(1).then(() => {
                if (pos >= readingBuffer.length) {
                    consumedBytes += readingBuffer.length
                    readingBuffer = buffers.splice(0, 1)[0] || []
                    pos = 0
                }
                return readingBuffer[pos++]
            })
        },
        peekByte: (offset) => {
            return checkBuffer(offset).then(() => {
                let p = pos + offset
                let readingB = readingBuffer
                let buffersI = 0
                if (p >= readingB.length) {
                    p -= readingB.length
                    readingB = buffers[buffersI]
                    buffersI++
                }
                return readingB[p]
            })
        },
        readBytes: (len) => {
            return checkBuffer(len).then(() => {
                if (pos + len < readingBuffer.length) {
                    let bytes = readingBuffer.subarray(pos, pos + len)
                    pos += len
                    return bytes
                }

                const bytes = new Uint8Array(len)
                let readedNum = 0
                while (buffers.length > 0 || readingBuffer.length > 0) {
                    if (pos + len > readingBuffer.length) {
                        const sub = readingBuffer.subarray(pos)
                        bytes.set(sub, readedNum)
                        readedNum += sub.length
                        len -= readedNum

                        if (!buffers[0]) {
                            return Promise.reject(new Error('Unexcepted EOF, pos=' + pos))
                        }

                        consumedBytes += readingBuffer.length
                        readingBuffer = buffers.splice(0, 1)[0]
                        pos = 0
                    } else {
                        bytes.set(readingBuffer.subarray(pos, pos + len), readedNum)
                        pos += len
                        break
                    }
                }
                return bytes
            })
        },
        peekBytes: (len) => {
            return checkBuffer(len).then(() => {
                if (pos + len < readingBuffer.length) {
                    return readingBuffer.subarray(pos, pos + len)
                }

                let p = pos
                let readingB = readingBuffer
                let buffersI = 0

                const bytes = new Uint8Array(len)
                let readedNum = 0
                while (buffers.length > buffersI || readingB.length > 0) {
                    if (p + len > readingB.length) {
                        const sub = readingB.subarray(p)
                        bytes.set(sub, readedNum)
                        readedNum += sub.length
                        len -= readedNum

                        if (!buffers[buffersI + 1]) {
                            // return Promise.reject(new Error('Unexcepted EOF: pos='+p))
                            return bytes.subarray(0, readedNum)
                        }

                        readingB = buffers[buffersI++]
                        p = 0
                    } else {
                        bytes.set(readingB.subarray(p, p + len), readedNum)
                        break
                    }
                }
                return bytes
            })
        },
        abort: () => {
            options.abort && options.abort()
        },
    }

    return that
}

export const buildStream = (loader) => ({
    loader: loader,
    pos: 0,
})

export const readByte = () => (stream) => {
    return stream.loader.readByte()
}

export const peekByte =
    (offset = 0) =>
    (stream) => {
        return stream.loader.peekByte(offset)
    }

export const readBytes = (length) => (stream) => {
    return stream.loader.readBytes(length)
}

export const peekBytes = (length) => (stream) => {
    return stream.loader.peekBytes(length)
}

export const readString = (length) => (stream) => {
    return readBytes(length)(stream).then((bytes) =>
        Array.from(bytes)
            .map((value) => String.fromCharCode(value))
            .join('')
    )
}

export const readUnsigned = (littleEndian) => (stream) => {
    return readBytes(2)(stream).then((bytes) => {
        return littleEndian ?
            (bytes[1] << 8) + bytes[0] :
            (bytes[0] << 8) + bytes[1]
    })
}

export const readArray =
    (byteSize, totalOrFunc) => (stream, result, parent) => {
        const totalPromise =
            typeof totalOrFunc === 'function' ?
            totalOrFunc(stream, result, parent) :
            totalOrFunc
        return Promise.resolve(totalPromise).then((total) => {
            const parser = readBytes(byteSize)
            const arr = new Array(total)
            let promiseLoop = Promise.resolve()
            for (var i = 0; i < total; i++) {
                promiseLoop = promiseLoop.then((e) => parser(stream))
                arr[i] = promiseLoop
            }

            return promiseLoop.then((e) => Promise.all(arr))
        })
    }

const subBitsTotal = (bits, startIndex, length) => {
    var result = 0
    for (var i = 0; i < length; i++) {
        result += bits[startIndex + i] && 2 ** (length - i - 1)
    }
    return result
}

export const readBits = (schema) => (stream) => {
    return readByte()(stream).then((byte) => {
        // convert the byte to bit array
        const bits = new Array(8)
        for (var i = 0; i < 8; i++) {
            bits[7 - i] = !!(byte & (1 << i))
        }
        // convert the bit array to values based on the schema
        return Object.keys(schema).reduce((res, key) => {
            const def = schema[key]
            if (def.length) {
                res[key] = subBitsTotal(bits, def.index, def.length)
            } else {
                res[key] = bits[def.index]
            }
            return res
        }, {})
    })
}